﻿using System;
using System.Collections.Generic;
using System.Text;
using System.Diagnostics;
using System.IO;
using System.Reflection;
using System.Net;
using Microsoft.CSharp;
using System.CodeDom.Compiler;

namespace CarseScript
{
    /// <summary>
    /// Root class for CarseScript.  Loads c# files into memory and can instantiate and invoke the
    /// scripts.  
    /// </summary>
    /// <remarks>
    /// Carse - Compiled At Runtime Scripting Environment
    /// Copyright @ Trevor Self 2015
    /// Source Code Released under the MIT License
    /// </remarks>
    public class Scripter
    {        
        // SETTINGS
        string compilerVersion;


        // DATA
        CompilerParameters compilerParameters;
        Dictionary<string, ScriptInfo> scriptsLib;


        /// <summary>
        /// Initializes a new instance of the ScriptService class with the specified parameters.
        /// </summary>
        public Scripter(string compilerVersion = "v4.0")
        {
            this.compilerVersion = compilerVersion;
            scriptsLib      = new Dictionary<string, ScriptInfo>();

            // SET UP INITIAL COMPILER PARAMETERS
            compilerParameters = new CompilerParameters
            {
                GenerateInMemory = true,
                GenerateExecutable = false
            };

            // Add CarseScript to assemblies accessible by scripts
            AddAssembly(typeof(Scripter).Assembly.Location);
        }


        /// <summary>
        /// Adds an assembly to the compiler parameters so that a script may use it with a using 
        /// statement.  
        /// </summary>
        /// <remarks>
        /// Assemblies that are located in a directory different from the mscorlib
        /// (i.e. C:\Windows\GAC) need a full path.  One easy way to do this is to use
        /// reflection and thus: 
        /// AddAssembly(typeof(myType).Assembly.Location);
        /// </remarks>
        /// <param name="name"></param>
        public void AddAssembly(string name)
        {
            compilerParameters.ReferencedAssemblies.Add(name);
        }


        /// <summary>
        /// Loads the scripts and compiles them.
        /// </summary>
        /// <param name="rootDirectory">directory to load scripts from</param>
        /// <param name="recursiveSearch">true/false, if true, searches all directories in the
        /// root directory recursively for scripts (.cs files) to load.</param>
        public void LoadScripts(bool recursiveSearch, string rootDirectory)
        {
            // create a temporary list of new scriptinfo
            List<ScriptInfo> scriptData = new List<ScriptInfo>();

            LoadFiles(recursiveSearch, rootDirectory, ref scriptData);
            CompileCode(ref scriptData);
            CheckForErrors(ref scriptData);
        }
        

        /// <summary>
        /// Loads all script files in the specified directory(s) into memory.
        /// </summary>
        private void LoadFiles(bool recursiveSearch, string rootDirectory, ref List<ScriptInfo> scriptData)
        {
            // Get all script file paths from the rootdirectory
            List<string> filePaths = new List<String>();
            filePaths.AddRange(SearchDirectories(rootDirectory, recursiveSearch));

            // process each new script
            

            ScriptInfo      newScript;

            for (int i = 0; i < filePaths.Count; i++)
            {                
                newScript = new ScriptInfo(
                    Path.GetFileNameWithoutExtension(filePaths[i]),
                    filePaths[i]);

                newScript.Process();

                scriptData.Add(newScript);

                scriptsLib.Add(newScript.Name, newScript);
            }
        }


        /// <summary>
        /// Compiles the strings into code.
        /// </summary>
        private void CompileCode(ref List<ScriptInfo> scriptData)
        {
            Dictionary<string, string> providerOptions = new Dictionary<string, string>
                {
                    {"CompilerVersion", "v4.0"}
                };

            CSharpCodeProvider provider = new CSharpCodeProvider(providerOptions);

            foreach (ScriptInfo script in scriptData)
                script.CompileCode(compilerParameters, provider);
        }


        /// <summary>
        /// Checks the compiled code for errors.
        /// </summary>
        private void CheckForErrors(ref List<ScriptInfo> scriptData)
        {
            bool hasErrors = false;
            StringBuilder errorBuilder = new StringBuilder();

            foreach (ScriptInfo script in scriptData)
            {
                if (script.HasErrors)
                {
                    hasErrors = true;
                    errorBuilder.AppendLine(script.Errors);
                }
            }

            if (hasErrors)
                throw new Exception(errorBuilder.ToString());
        }
        

        /// <summary>
        /// Searches a directory and gets all files with the .cs extension.
        /// </summary>
        /// <param name="sDirStart">starting directory</param>
        /// <returns></returns>
        private List<string> SearchDirectories(string sDirStart, bool isRecursive)
        {
            List<string> files = new List<string>();

            foreach (string f in Directory.GetFiles(sDirStart))
            {
                if (Path.GetExtension(f) == ".cs")
                {
                    files.Add(f);
                }
            }

            if (isRecursive)
            {
                foreach (string d in Directory.GetDirectories(sDirStart))
                {
                    files.AddRange(SearchDirectories(d, true));
                }
            }

            return files;
        }


        /// <summary>
        /// Gets a script object.
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="name"></param>
        /// <returns></returns>
        private T GetScriptObject<T>(string name)
        {
            // check if this name is used
            if (!scriptsLib.ContainsKey(name))
                throw new Exception("No such script: " + name);

            CompilerResults compiledCode = scriptsLib[name].CompiledCode;

            try
            {
                // use compiler results to create an instance of the script:
                object o = compiledCode.CompiledAssembly.CreateInstance(name);
                
                if (o == null)
                    throw new Exception("Attempted to invoke script: " + name
                        + " but no such script has been loaded.");

                if (!(o is T))
                    throw new Exception("Attempted to cast script " + name
                        + " into type " + typeof(T).Name + " but script is not of that type");

                return (T)o;
            }
            catch (Exception e)
            {
                throw e;
            }
        }


        /// <summary>
        /// Invokes the script by the script name.  Returns the script after Main() has been called.
        /// </summary>
        /// <param name="name">name of script file, no path</param>
        /// <param name="objectData">object data, null for systemscripts</param>
        public IScript InvokeScript(string name, object[] objectData)
        {
            var script = GetScriptObject<IScript>(name);
            script.Initialize(objectData);
            script.Main();

            return script;   
        }
    }
}
